package com.eh.encription;

import com.eh.encription.ext.RsaEncryptData;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.io.IOException;
import java.security.*;

/**
 * Created by David Li on 2016/4/14.
 */
public class EncryptUtil {
    private static String DES_KEY = "12345678"; // DES密钥长度至少8位
    private static KeyPair keyPair = generateRSAKeyPair();

    /**
     * 产生RSA公私钥对
     */
    public static KeyPair generateRSAKeyPair() {
        KeyPairGenerator rsaKeyGen;
        KeyPair rsaKeyPair = null;
        try {
            rsaKeyGen = KeyPairGenerator.getInstance("RSA");
            SecureRandom random = new SecureRandom();
            random.nextBytes(new byte[1]);
            rsaKeyGen.initialize(1024, new SecureRandom());
            rsaKeyPair = rsaKeyGen.genKeyPair();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return rsaKeyPair;
    }

    /**
     * MD5单向加密
     *
     * @param plainText
     * @return
     * @throws NoSuchAlgorithmException
     * @detail 消息摘要算法第五版(Message Digest Algorithm)，是一种单向加密算法，只能加密、无法解密。然而MD5加密算法已经被中国山东大学王小云教授成功破译，
     * 但是在安全性要求不高的场景下，MD5加密算法仍然具有应用价值。
     */
    public static String encryptWithMD5(String plainText) throws NoSuchAlgorithmException {
        // 1. 创建MD5对象
        MessageDigest md5 = MessageDigest.getInstance("md5");
        // 2. 进行加密操作
        byte[] cipherData = md5.digest(plainText.getBytes());
        StringBuilder sb = new StringBuilder();
        // 3. 将其中的每个字节转换成十六进制字符串，byte类型的数据最高位是符号位，通过和0xff进行与操作，转换为int类型的正整数
        for (byte cipher : cipherData) {
            String toHexStr = Integer.toHexString(cipher & 0xff);
            // 4. 如果该正数小于16位(长度为1个字符)，前面拼接0占位：确保最后生成的是32位字符串
            sb.append(toHexStr.length() == 1 ? "0" + toHexStr : toHexStr);
        }
        return sb.toString();
    }

    /**
     * 使用BASE64进行加密
     *
     * @param plainText
     * @return
     * @detail 使用BASE64算法通常用作对二进制数据进行加密，加密之后的数据不易被肉眼识别。严格来说，经过BASE64加密的数据其实没有安全性可言，因为它的加密解密
     * 算法都是公开的，典型的防菜鸟不防程序猿的呀。 经过标准的BASE64算法加密后的数据， 通常包含/、+、=等特殊符号，不适合作为url参数传递，幸运的是Apache的
     * Commons Codec模块提供了对BASE64的进一步封装。
     */
    public static String encryptWithBase64(String plainText) {
        // 使用BASE64加密
        BASE64Encoder encoder = new BASE64Encoder();
        String cipherText = encoder.encode(plainText.getBytes());
        return cipherText;
    }

    /**
     * 使用BASE64进行解密
     *
     * @param cipherText
     * @return
     * @throws IOException
     */
    public static String decryptWithBase64(String cipherText) throws IOException {
        // 使用BASE64解密
        BASE64Decoder decoder = new BASE64Decoder();
        String plainText = new String(decoder.decodeBuffer(cipherText));
        return plainText;
    }

    /**
     * 使用DES对称加密
     *
     * @param plainText
     * @return
     * @throws Exception
     * @detail 数据加密标准算法(Data Encryption Standard)，和BASE64最明显的区别就是有一个工作密钥，该密钥既用于加密、也用于解密，并且要求密钥是一个长度至少
     * 大于8位的字符串。使用DES加密、解密的核心是确保工作密钥的安全性。
     */
    public static byte[] encryptWithDES(String plainText) throws Exception {
        // 1. 根据key生成密钥
        SecureRandom random = new SecureRandom();
        DESKeySpec keySpec = new DESKeySpec(DES_KEY.getBytes());
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("des");
        SecretKey secretKey = keyFactory.generateSecret(keySpec);

        // 2. 加密操作
        Cipher cipher = Cipher.getInstance("des");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, random);
        byte[] cipherData = cipher.doFinal(plainText.getBytes());

        // 3. 为了便于观察生成的加密数据，使用BASE64再次加密
        // String cipherText = new BASE64Encoder().encode(cipherData);

        return cipherData;
    }

    /**
     * 使用DES对称解密
     *
     * @param cipherData
     * @return
     * @throws Exception
     */
    public static String decryptWithDES(byte[] cipherData) throws Exception {
        // 1. 根据key生成密钥
        SecureRandom random = new SecureRandom();
        DESKeySpec keySpec = new DESKeySpec(DES_KEY.getBytes());
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("des");
        SecretKey secretKey = keyFactory.generateSecret(keySpec);

        // 2. 解密操作
        Cipher cipher = Cipher.getInstance("des");
        cipher.init(Cipher.DECRYPT_MODE, secretKey, random);
        byte[] plainData = cipher.doFinal(cipherData);
        return new String(plainData);
    }

    /**
     * 使用RSA非对称加密
     *
     * @param plainText
     * @return
     * @throws Exception
     * @detail RSA算法是非对称加密算法的典型代表，既能加密、又能解密。和对称加密算法比如DES的明显区别在于用于加密、解密的密钥是不同的。使用RSA算法，
     * 只要密钥足够长(一般要求1024bit)，加密的信息是不能被破解的。用户通过https协议访问服务器时，就是使用非对称加密算法进行数据的加密、解密操作的。
     * 服务器发送数据给客户端时使用私钥（private key）进行加密，并且使用加密之后的数据和私钥生成数字签名（digital signature）并发送给客户端。
     * 客户端接收到服务器发送的数据会使用公钥（public key）对数据来进行解密，并且根据加密数据和公钥验证数字签名的有效性，
     * 防止加密数据在传输过程中被第三方进行了修改。客户端发送数据给服务器时使用公钥进行加密，服务器接收到加密数据之后使用私钥进行解密。
     */
    public static RsaEncryptData encryptWithRSA(String plainText) throws Exception {
        // 1. 服务器数据使用私钥加密
        RsaEncryptData rsaEncryptData = new RsaEncryptData();

        PrivateKey privateKey = keyPair.getPrivate();

        Cipher cipher = Cipher.getInstance("rsa");
        SecureRandom random = new SecureRandom();

        cipher.init(Cipher.ENCRYPT_MODE, privateKey, random);
        byte[] cipherData = cipher.doFinal(plainText.getBytes());

        // 2. 服务器根据私钥和加密数据生成数字签名
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initSign(privateKey);
        signature.update(cipherData);
        byte[] signData = signature.sign();

        rsaEncryptData.setCipherData(cipherData);
        rsaEncryptData.setSignData(signData);

        return rsaEncryptData;
    }

    /**
     * 使用RSA非对称解密
     *
     * @param rsaEncryptData
     * @return
     * @throws Exception
     */
    public static String decryptWithRSA(RsaEncryptData rsaEncryptData) throws Exception {
        // 1. 用户根据公钥、加密数据验证数据是否被修改过
        Signature signature = Signature.getInstance("MD5withRSA");

        signature.initVerify(keyPair.getPublic());
        signature.update(rsaEncryptData.getCipherData());
        boolean status = signature.verify(rsaEncryptData.getSignData());

        if (!status) {
            throw new RuntimeException("根据公钥、加密数据验证数据已经被修改过！");
        }

        // 2. 用户使用公钥解密
        Cipher cipher = Cipher.getInstance("rsa");
        SecureRandom random = new SecureRandom();

        cipher.init(Cipher.DECRYPT_MODE, keyPair.getPublic(), random);
        byte[] plainData = cipher.doFinal(rsaEncryptData.getCipherData());

        return new String(plainData);
    }
}
